/**
 * Title: jFamilyTree provider
 * Description: jFamilyTree Provider
 * Version: provider
 * Copyright: (c) 2001-2008
 * Terms of use:see license agreement at http://treebolic.sourceforge.net/en/license.htm
 * Author: Bernard Bou
 * Company: bsys
 * Update: Mon Mar 10 00:00:00 CEST 2008
 */

package jFamilyTree.provider.xml.fast;

import java.awt.Color;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Stack;

import jFamilyTree.control.Mounter;
import jFamilyTree.model.Tree;
import jFamilyTree.model.IEdge;
import jFamilyTree.model.MenuItem;
import jFamilyTree.model.Model;
import jFamilyTree.model.MountPoint;
import jFamilyTree.model.MutableEdge;
import jFamilyTree.model.MutableNode;
import jFamilyTree.model.Node;
import jFamilyTree.model.Utils;

/**
 * Light-weight XML parser for jFamilyTree
 * 
 * @author Bernard Bou
 */
class Parser
{
	// D A T A

	/**
	 * Source URL
	 */
	private URL theURL;

	/**
	 * Provider
	 */
	private Provider theProvider;

	// model

	/**
	 * Root node
	 */
	private MutableNode theRoot = null;

	/**
	 * Edge list
	 */
	private List<IEdge> theEdges = null;

	/**
	 * Id to node map
	 */
	private Hashtable<String, MutableNode> theNodesById = new Hashtable<String, MutableNode>();

	// settings

	/**
	 * Settings
	 */
	private jFamilyTree.model.Settings theSettings = new jFamilyTree.model.Settings();

	/**
	 * Menu
	 */
	private List<MenuItem> theMenu;

	/**
	 * Menu item
	 */
	private MenuItem theMenuItem;

	// reader

	/**
	 * Reader
	 */
	private BufferedReader theReader;

	// scope

	/**
	 * Scope types
	 */
	private static enum Scope
	{
		NONE, TREEBOLIC, TREE, NODES, NODE, TREEEDGE, EDGES, EDGE, DEFAULTTREEEDGE, DEFAULTEDGE, MOUNTPOINT, MENU
	};

	// C O N S T R U C T O R

	/**
	 * Constructor
	 * 
	 * @param thisURL
	 *        source url
	 * @param thisProvider
	 *        provider
	 */
	public Parser(URL thisURL, Provider thisProvider)
	{
		theRoot = null;
		theEdges = null;
		theSettings = null;

		theURL = thisURL;
		theProvider = thisProvider;

		try
		{
			theReader = new BufferedReader(new InputStreamReader(theURL.openStream(), "UTF8"));
		}
		catch (SecurityException e)
		{
			System.err.println("Xml parser: Access refused for " + thisURL.toString() + ":" + e.getMessage());
			e.printStackTrace();
		}
		catch (FileNotFoundException e)
		{
			System.err.println("Xml parser: File not found on " + thisURL.toString() + ":" + e.getMessage());
			return;
		}
		catch (Throwable e)
		{
			System.err.println("Xml parser: Exception on " + thisURL.toString() + ":" + e.getMessage());
			e.printStackTrace();
			return;
		}
		return;
	}

	/**
	 * Whether reader is valid
	 * 
	 * @return true if reader is valid
	 */
	public boolean isValid()
	{
		return theReader != null;
	}

	// R E A D E R
	// implements basic PushbackReader with one-character lookahead capabilities

	/**
	 * Character lookahead
	 */
	private int lookahead = -1;

	/**
	 * Read character
	 * 
	 * @return read character
	 */
	private int read()
	{
		int c;
		if (lookahead != -1)
		{
			c = lookahead;
			lookahead = -1;
			return c;
		}
		try
		{
			c = theReader.read();
			return c;
		}
		catch (IOException e)
		{
			return -1;
		}
	}

	/**
	 * Unread (=push back) character
	 * 
	 * @param c
	 *        character
	 */
	private void unread(int c)
	{
		lookahead = c;
	}

	// T A G . A N D . T E X T . E X T R A C T I O N

	/**
	 * Whether more tags are available
	 * 
	 * @return true if more tags are available
	 */
	private boolean hasMoreTags()
	{
		int c;
		do
		{
			c = read();
			if (c == -1)
			{
				return false;
			}
		}
		while (c != '<');
		unread(c);
		return true;
		// out assert : next read yields '<'
	}

	/**
	 * Get next tag string
	 * 
	 * @return next tag string
	 */
	private String getNextTag()
	{
		// in assert : next read yields '<'
		// eat '<'
		int c = read();
		if (c != '<')
		{
			// unexpected
			return null;
		}

		// accumulate
		StringBuffer thisBuffer = new StringBuffer();
		while ((c = read()) != -1)
		{
			if (c == '>')
			{
				// out assert : next read is past '>'
				return thisBuffer.toString();
			}
			thisBuffer.append((char) c);

			// CDATA
			if (thisBuffer.length() == 8 && thisBuffer.indexOf("![CDATA[", 0) != -1)
			{
				while ((c = read()) != -1)
				{
					thisBuffer.append((char) c);
					if (c == ']')
						break;
				}
				if (c == -1)
					System.err.println("Xml parser: Unterminated CDATA: " + thisBuffer.toString());
			}
		}
		// unexpected EOF
		return null;
	}

	/**
	 * Get tag text
	 * 
	 * @return tag text
	 */
	private String getText()
	{
		// between thisTag.opening and closing tag
		StringBuffer thisBuffer = new StringBuffer();
		for (int c; (c = read()) != -1;)
		{
			if (c == '<')
			{
				// out assert : next read yields '<'
				unread(c);
				return Entities.unescapeXml(thisBuffer.toString());
			}
			thisBuffer.append((char) c);
		}
		// unexpected EOF
		return null;
	}

	// P A R S E . T A G S

	/**
	 * Parse <jFamilyTree> tag
	 * 
	 * @param thisTag
	 *        tag string
	 */
	private void parsejFamilyTreeTag(Tag thisTag)
	{
		String thisParam;
		thisParam = thisTag.getAttribute("toolbar");
		if (thisParam != null)
			theSettings.theHasToolbarFlag = Boolean.valueOf(thisParam);
		thisParam = thisTag.getAttribute("statusbar");
		if (thisParam != null)
			theSettings.theHasStatusbarFlag = Boolean.valueOf(thisParam);
		thisParam = thisTag.getAttribute("popupmenu");
		if (thisParam != null)
			theSettings.theHasPopUpMenuFlag = Boolean.valueOf(thisParam);
		thisParam = thisTag.getAttribute("tooltip");
		if (thisParam != null)
			theSettings.theHasToolTipFlag = Boolean.valueOf(thisParam);
		thisParam = thisTag.getAttribute("tooltip-displays-content");
		if (thisParam != null)
			theSettings.theToolTipDisplaysContentFlag = Boolean.valueOf(thisParam);
		thisParam = thisTag.getAttribute("focus");
		if (thisParam != null)
			theSettings.theFocus = thisParam;
		thisParam = thisTag.getAttribute("focus-on-hover");
		if (thisParam != null)
			theSettings.theFocusOnHoverFlag = Boolean.valueOf(thisParam);
		thisParam = thisTag.getAttribute("xmoveto");
		if (thisParam != null)
			theSettings.theXMoveTo = Double.valueOf(thisParam).doubleValue();
		thisParam = thisTag.getAttribute("ymoveto");
		if (thisParam != null)
			theSettings.theYMoveTo = Double.valueOf(thisParam).doubleValue();
		thisParam = thisTag.getAttribute("xshift");
		if (thisParam != null)
			theSettings.theXShift = Double.valueOf(thisParam).doubleValue();
		thisParam = thisTag.getAttribute("yshift");
		if (thisParam != null)
			theSettings.theYShift = Double.valueOf(thisParam).doubleValue();

	}

	/**
	 * Parse <tree> tag
	 * 
	 * @param thisTag
	 *        tag string
	 */
	private void parseTreeTag(Tag thisTag)
	{
		Color thisColor;
		thisColor = Utils.stringToColor(thisTag.getAttribute("backcolor"));
		if (thisColor != null)
			theSettings.theBackColor = thisColor;
		thisColor = Utils.stringToColor(thisTag.getAttribute("forecolor"));
		if (thisColor != null)
			theSettings.theForeColor = thisColor;
		String thisParameter;
		thisParameter = thisTag.getAttribute("orientation");
		if (thisParameter != null)
			theSettings.theOrientation = thisParameter;
		thisParameter = thisTag.getAttribute("expansion");
		if (thisParameter != null)
			theSettings.theExpansion = Double.valueOf(thisParameter).doubleValue();
		thisParameter = thisTag.getAttribute("sweep");
		if (thisParameter != null)
			theSettings.theSweep = Double.valueOf(thisParameter).doubleValue();
		thisParameter = thisTag.getAttribute("preserve-orientation");
		if (thisParameter != null)
			theSettings.thePreserveOrientationFlag = Boolean.valueOf(thisParameter);
		thisParameter = thisTag.getAttribute("fontface");
		if (thisParameter != null)
			theSettings.theFontFace = thisParameter;
		thisParameter = thisTag.getAttribute("fontsize");
		if (thisParameter != null)
			theSettings.theFontSize = Integer.valueOf(thisParameter).intValue();
		thisParameter = thisTag.getAttribute("fontsizestep");
		if (thisParameter != null)
			theSettings.theFontSizeStep = Integer.valueOf(thisParameter).intValue();
	}

	/**
	 * Parse <nodes> tag
	 * 
	 * @param thisTag
	 *        tag string
	 */
	private void parseNodesTag(Tag thisTag)
	{
		Color thisColor;
		thisColor = Utils.stringToColor(thisTag.getAttribute("backcolor"));
		if (thisColor != null)
			theSettings.theNodeBackColor = thisColor;
		thisColor = Utils.stringToColor(thisTag.getAttribute("forecolor"));
		if (thisColor != null)
			theSettings.theNodeForeColor = thisColor;
	}

	/**
	 * Parse <nodesc> tag
	 * 
	 * @param thisTag
	 *        tag string
	 */
	private void parseEdgesTag(Tag thisTag)
	{
		String thisParam;
		thisParam = thisTag.getAttribute("hyperbolic");
		if (thisParam != null)
			theSettings.theHyperbolicEdgesFlag = Boolean.valueOf(thisParam);
	}

	/**
	 * Parse <edge> tag
	 * 
	 * @param thisTag
	 *        tag string
	 * @return edge
	 */
	private MutableEdge parseEdgeTag(Tag thisTag)
	{
		String thisFromId = thisTag.getAttribute("from");
		MutableNode thisFrom = theNodesById.get(thisFromId);

		String thisToId = thisTag.getAttribute("to");
		MutableNode thisTo = theNodesById.get(thisToId);

		// make new edge
		if (thisFrom != null && thisTo != null)
		{
			MutableEdge thisEdge = new MutableEdge(thisFrom, thisTo);

			Color thisColor = Utils.stringToColor(thisTag.getAttribute("color"));
			if (thisColor != null)
				thisEdge.setColor(thisColor);
			Integer thisStyle = Utils.parseStyle(thisTag.getAttribute("stroke"), thisTag.getAttribute("toterminator"), thisTag.getAttribute("fromterminator"), thisTag.getAttribute("hidden"));
			if (thisStyle != null && thisStyle != 0)
				thisEdge.setStyle(thisStyle);
			return thisEdge;
		}
		else
		{
			System.err.println("Xml parser: Malformed edge:" + thisTag);
		}
		return null;
	}

	// P A R S E

	/**
	 * Parse model
	 * 
	 * @return model, null if fail
	 */
	public Model parseModel()
	{
		Tree thisTree = parse(true);
		return thisTree != null ? new Model(thisTree, theSettings) : null;
	}

	/**
	 * Parse tree
	 * 
	 * @return tree, null if fail
	 */
	public Tree parseTree()
	{
		return parse(false);
	}

	/**
	 * Parse. Settings are built as a side-effect.
	 * 
	 * @param doSettings
	 *        whether to consider or discard settings
	 * @return graph if successful
	 */
	private Tree parse(boolean doSettings)
	{
		theSettings = doSettings ? new jFamilyTree.model.Settings() : null;
		Stack<MutableNode> thisNodeStack = new Stack<MutableNode>();
		MutableEdge thisEdge = null;
		Scope thisScope = Scope.NONE;
		String thisCData = null;
		boolean mountNow = false;

		while (hasMoreTags())
		{
			// tag string
			String thisTagString = getNextTag();
			if (thisTagString == null)
				break;

			// parse tag
			Tag thisTag = Tag.makeTag(thisTagString);
			if (thisTag == null)
			{
				System.err.println("UNPARSED TAG:" + thisTagString);
				continue;
			}

			// N O D E
			// (changes scope)
			if (thisTag.theName.equals("node"))
			{
				if (thisTag.open)
				{
					String thisId = thisTag.getAttribute("id");
					MutableNode thisNode;
					if (!thisNodeStack.empty())
					{
						Node thisParent = thisNodeStack.peek();
						thisNode = new MutableNode(thisParent, thisId);
					}
					else
					{
						thisNode = new MutableNode(null, thisId);
						theRoot = thisNode;
					}
					theNodesById.put(thisId, thisNode);

					// attributes
					Color thisForeColor = Utils.stringToColor(thisTag.getAttribute("forecolor"));
					thisNode.setForeColor(thisForeColor);
					Color thisBackColor = Utils.stringToColor(thisTag.getAttribute("backcolor"));
					thisNode.setBackColor(thisBackColor);
					String thisWeight = thisTag.getAttribute("weight");
					if (thisWeight != null && thisWeight.isEmpty())
						thisNode.setWeight(-Double.valueOf(thisWeight).doubleValue());

					// tracking
					if (thisTag.unclosed)
					{
						thisNodeStack.push(thisNode);
						thisScope = Scope.NODE;
					}
					else
					{
						// standalone
						thisScope = thisNodeStack.isEmpty() ? Scope.NONE : Scope.NODE;
					}
				}
				else
				{
					// close
					thisNodeStack.pop();
					thisScope = thisNodeStack.isEmpty() ? Scope.NONE : Scope.NODE;
				}
			}


			// L A B E L
			// (does not change scope)
			// (scope dependent)
			else if (thisTag.theName.equals("label"))
			{
				if (thisTag.open)
				{
					switch (thisScope)
					{
					case MENU:
						if (doSettings)
						{
							String thisLabel = getText();
							theMenuItem.theLabel = thisLabel != null ? thisLabel : "?";
						}
						break;

					case NODE:
						if (!thisNodeStack.empty())
						{
							String thisLabel = getText();
							if (thisLabel != null)
							{
								MutableNode thisNode = thisNodeStack.peek();
								thisNode.setLabel(thisLabel);
							}
							break;
						}
					case TREEEDGE:
						if (!thisNodeStack.empty())
						{
							String thisLabel = getText();
							if (thisLabel != null)
							{
								MutableNode thisNode = thisNodeStack.peek();
								thisNode.setEdgeLabel(thisLabel);
							}
							break;
						}

					case EDGE:
						String thisLabel = getText();
						if (thisLabel != null && thisEdge != null)
							thisEdge.setLabel(thisLabel);
						break;

					default:
						System.err.println("Xml parser: Tag error: " + thisTag + " scope=" + thisScope);
					}
				}
				else
				{
					// get embedded cdata text
					if (thisCData != null)
					{
						if (!thisNodeStack.empty())
						{
							MutableNode thisNode = thisNodeStack.peek();
							thisNode.setLabel(thisCData);
						}
					}
					// invalidate cdata
					thisCData = null;
				}

			}

			// C O N T E N T
			// (does not change scope)
			else if (thisTag.theName.equals("content"))
			{
				if (thisTag.open)
				{
					if (!thisNodeStack.empty())
					{
						// get content text
						String thisContent = getText();
						if (thisContent != null)
						{
							MutableNode thisNode = thisNodeStack.peek();
							thisNode.setContent(thisContent);
						}
					}
				}
				else
				{
					// get embedded cdata text
					if (thisCData != null)
					{
						if (!thisNodeStack.empty())
						{
							MutableNode thisNode = thisNodeStack.peek();
							thisNode.setContent(thisCData);
						}
					}
					// invalidate cdata
					thisCData = null;
				}
			}

			// I M A G E
			// (does not change scope)
			// (scope dependent)
			else if (thisTag.theName.equals("img"))
			{
				if (thisTag.open)
				{
					String thisParam = thisTag.getAttribute("src");
					if (thisParam != null)
					{
						switch (thisScope)
						{
						case NODE:
							if (!thisNodeStack.empty())
							{
								MutableNode thisNode = thisNodeStack.peek();
								thisNode.setImageFile(thisParam);
							}
							break;

						case TREEEDGE:
							if (!thisNodeStack.empty())
							{
								MutableNode thisNode = thisNodeStack.peek();
								thisNode.setEdgeImageFile(thisParam);
							}
							break;

						case EDGE:
							if (thisEdge != null)
								thisEdge.setImageFile(thisParam);
							break;

						case DEFAULTTREEEDGE:
							if (doSettings)
								theSettings.theDefaultTreeEdgeImage = thisParam;
							break;

						case DEFAULTEDGE:
							if (doSettings)
								theSettings.theDefaultEdgeImage = thisParam;
							break;

						case NODES:
							if (doSettings)
								theSettings.theDefaultNodeImage = thisParam;
							break;

						case TREE:
							if (doSettings)
								theSettings.theBackgroundImage = thisParam;
							break;

						default:
							System.err.println("Xml parser: Tag error: " + thisTag + " scope=" + thisScope);
						}
					}
				}
			}

			// L I N K
			// (does not change scope)
			// (scope dependent)
			else if (thisTag.theName.equals("a"))
			{
				if (thisTag.open)
				{
					switch (thisScope)
					{
					case MOUNTPOINT:
						if (!thisNodeStack.empty())
						{
							String thisHrefParam = thisTag.getAttribute("href");
							if (thisHrefParam != null)
							{
								Node thisNode = thisNodeStack.peek();
								MountPoint.Mounting thisMountPoint = (MountPoint.Mounting) thisNode.getMountPoint();
								thisMountPoint.theURL = thisHrefParam;
							}
						}
						break;

					case MENU:
						if (doSettings)
						{
							theMenuItem.theLink = thisTag.getAttribute("href");
							theMenuItem.theTarget = thisTag.getAttribute("target");
						}
						break;

					case NODE:
						if (!thisNodeStack.empty())
						{
							String thisHrefParam = thisTag.getAttribute("href");
							String thisTargetParam = thisTag.getAttribute("target");
							if (thisHrefParam != null)
							{
								MutableNode thisNode = thisNodeStack.peek();
								thisNode.setLink(thisHrefParam);
								thisNode.setTarget(thisTargetParam);
							}
						}
						break;

					default:
						System.err.println("Xml parser: Tag error: " + thisTag + " scope=" + thisScope);
					}
				}
			}

			// T R E E . E D G E
			// (changes scope)
			else if (thisTag.theName.equals("treeedge"))
			{
				if (thisTag.open)
				{
					if (!thisNodeStack.empty())
					{
						MutableNode thisNode = thisNodeStack.peek();

						Color thisColor;
						thisColor = Utils.stringToColor(thisTag.getAttribute("color"));
						if (thisColor != null)
							thisNode.setEdgeColor(thisColor);

						Integer thisStyle = Utils.parseStyle(thisTag.getAttribute("stroke"), thisTag.getAttribute("toterminator"), thisTag.getAttribute("fromterminator"), thisTag.getAttribute("hidden"));
						if (thisStyle != null && thisStyle != 0)
							thisNode.setEdgeStyle(thisStyle);
					}
					thisScope = thisTag.unclosed ? Scope.TREEEDGE : Scope.NODE;
				}
				else
					thisScope = Scope.NODE;
			}

			// N O D E S
			// (changes scope)
			else if (thisTag.theName.equals("nodes"))
			{
				if (thisTag.open)
				{
					if (doSettings)
						parseNodesTag(thisTag);
					thisScope = thisTag.unclosed ? Scope.NODES : Scope.NONE;
				}
				else
					thisScope = Scope.NONE;
			}

			// E D G E
			// (changes scope)
			else if (thisTag.theName.equals("edge"))
			{
				if (thisTag.open)
				{
					thisEdge = parseEdgeTag(thisTag);
					if (thisEdge != null)
					{
						if (theEdges == null)
							theEdges = new ArrayList<IEdge>();
						theEdges.add(thisEdge);
					}
					thisScope = thisTag.unclosed ? Scope.EDGE : Scope.NONE;
				}
				else
					thisScope = Scope.NONE;
			}

			// E D G E S
			// (changes scope)
			else if (thisTag.theName.equals("edges"))
			{
				if (thisTag.open)
				{
					if (doSettings)
						parseEdgesTag(thisTag);
					thisScope = thisTag.unclosed ? Scope.EDGES : Scope.NONE;
				}
				else
					thisScope = Scope.NONE;
			}

			// T R E E
			// (changes scope)
			else if (thisTag.theName.equals("tree"))
			{
				if (thisTag.open)
				{
					if (doSettings)
						parseTreeTag(thisTag);
					thisScope = thisTag.unclosed ? Scope.TREE : Scope.NONE;
				}
				else
					thisScope = Scope.NONE;
			}

			// T R E E B O L I C
			// (changes scope)
			else if (thisTag.theName.equals("jfamilytree"))
			{
				if (thisTag.open)
				{
					if (doSettings)
						parsejFamilyTreeTag(thisTag);
					thisScope = thisTag.unclosed ? Scope.TREEBOLIC : Scope.NONE;
				}
				else
				{
					thisScope = Scope.NONE;
					break;
				}
			}

			// M O U N T P O I N T
			// (changes scope)
			else if (thisTag.theName.equals("mountpoint"))
			{
				if (thisTag.open)
				{
					thisScope = thisTag.unclosed ? Scope.MOUNTPOINT : Scope.NODE;
					if (!thisNodeStack.empty())
					{
						Node thisNode = thisNodeStack.peek();
						MountPoint.Mounting thisMountPoint = new MountPoint.Mounting();
						thisNode.setMountPoint(thisMountPoint);

						// mount now
						String thisParam = thisTag.getAttribute("now");
						mountNow = thisParam != null && thisParam.equalsIgnoreCase("true");
					}
				}
				else
				{
					if (!thisNodeStack.empty())
					{
						Node thisNode = thisNodeStack.peek();
						MountPoint.Mounting thisMountingPoint = (MountPoint.Mounting) thisNode.getMountPoint();
						if (thisMountingPoint != null)
						{
							// mount now ?
							if (mountNow)
							{
								// mount
								Tree thisTree = theProvider.makeTree(thisMountingPoint.theURL, true);
								if (thisTree != null)
									Mounter.graft(thisNode, thisTree.getRoot(), theEdges, thisTree.getEdges());
							}
							mountNow = false;
						}
					}
					thisScope = Scope.NODE;
				}
			}

			// D E F A U L T . T R E E E D G E
			// (changes scope)
			else if (thisTag.theName.equals("default.treeedge"))
			{
				if (thisTag.open)
				{
					thisScope = Scope.DEFAULTTREEEDGE;
					if (doSettings)
					{
						Integer thisStyle = Utils.parseStyle(thisTag.getAttribute("stroke"), thisTag.getAttribute("toterminator"), thisTag.getAttribute("fromterminator"), thisTag.getAttribute("hidden"));
						if (thisStyle != null && thisStyle != 0)
							theSettings.theTreeEdgeStyle = thisStyle;
						Color thisColor = Utils.stringToColor(thisTag.getAttribute("color"));
						if (thisColor != null)
							theSettings.theTreeEdgeColor = thisColor;
					}
				}
				else
					thisScope = Scope.NONE;
			}

			// D E F A U L T . E D G E
			// (changes scope)
			else if (thisTag.theName.equals("default.edge"))
			{
				if (thisTag.open)
				{
					thisScope = Scope.DEFAULTEDGE;
					if (doSettings)
					{
						Integer thisStyle = Utils.parseStyle(thisTag.getAttribute("stroke"), thisTag.getAttribute("toterminator"), thisTag.getAttribute("fromterminator"), thisTag.getAttribute("hidden"));
						if (thisStyle != null && thisStyle != 0)
							theSettings.theEdgeStyle = thisStyle;
						Color thisColor = Utils.stringToColor(thisTag.getAttribute("color"));
						if (thisColor != null)
							theSettings.theEdgeColor = thisColor;
					}
				}
				else
					thisScope = Scope.NONE;
			}

			// M E N U
			// (changes scope)
			else if (thisTag.theName.equals("menu"))
			{
				if (thisTag.open)
				{
					thisScope = Scope.MENU;
					if (doSettings)
						theMenu = new ArrayList<MenuItem>();
				}
				else
				{
					thisScope = Scope.NONE;
					if (doSettings)
						theSettings.theMenu = theMenu;
				}
			}

			// M E N U . I T E M
			// (does not change scope)
			else if (thisTag.theName.equals("menuitem"))
			{
				if (thisTag.open)
				{
					if (doSettings)
					{
						theMenuItem = new MenuItem();
						Utils.parseMenuItem(theMenuItem, thisTag.getAttribute("action"), thisTag.getAttribute("match-scope"), thisTag.getAttribute("match-mode"));

						// match target
						String thisAttribute = thisTag.getAttribute("match-target");
						theMenuItem.theMatchTarget = thisAttribute;
					}
					else if (doSettings)
						theMenu.add(theMenuItem);
				}
			}
			else if (thisTag.theName.equals("tools"))
			{
			}

			// O T H E R S
			else if (thisTag.theName.startsWith("!"))
			{
				if (thisTagString.startsWith("![CDATA["))
				{
					thisCData = thisTagString.substring(8, thisTagString.length() - 2);
				}
			}
			else if (thisTag.theName.startsWith("?"))
			{
				System.out.println("[?]" + thisTag);
			}
			else
			{
				System.err.println("Xml parser: Unhandled tag: " + thisTag);
				return null;
			}
		}
		return new Tree(theRoot, theEdges);
	}
}
